Skip to content

[OMNIML-6072] [2/21] Make amax synchronization correct under data-parallel calibration - #2744

Open
shengliangxu wants to merge 11 commits into
mainfrom
shengliangx/hfep-7-amax-sync
Open

shengliangxu wants to merge 11 commits into
mainfrom
shengliangx/hfep-7-amax-sync

Conversation

@shengliangxu

@shengliangxu shengliangxu commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

What does this PR do?

Type of change: Bug fix (for data-parallel calibration, which the series introduces)

With data-parallel calibration a module can be exercised on some ranks and not others: a sparsely routed MoE expert, or anything a given rank's batch never reaches. Its _amax then exists on only part of the group. Every rank must still join the collective, or the participant set mismatches and NCCL hangs. So the whole question is what a rank with no amax contributes.

  • Per-tensor amax. The shape is known locally, so a rank without one contributes a scalar zero, the identity for MAX over non-negative amax.
    • The same all-reduce carries a presence flag and the calibrator's dtype.
    • If nobody calibrated the quantizer, it stays without an amax, so the usual amax is None handling still applies.
    • A calibrated amax of zero, from an all-zero weight, survives as on main.
  • Static per-channel or per-block amax. A rank without one cannot know its shape, so ranks first agree, through one MAX all-reduce of a presence flag that every rank joins. If nobody calibrated it, it is skipped. If everybody did, the real reduce runs. A genuine split raises on every rank at once; raising only where the amax is missing would leave the other ranks blocked in a collective that never completes.
  • Dynamic-block quantizers (NVFP4 activations) store only a scalar global scale, so they are treated as per-tensor rather than pushed into the static path's hard error.
  • Dtype and device.
    • Every rank contributes float32, on the device of the group's backend: NCCL hangs rather than raising on a dtype mismatch. This was seen on Kimi-K3, NVFP4 EP64, with 8 ranks holding an amax and 56 not.
    • Each rank then keeps its calibrator's dtype, so later bf16 writes (MSE calibration) round exactly as on main. A rank that never calibrated the quantizer adopts the group's dtype.

max_calibrate keeps main's sync structure (data-parallel always, expert-parallel filtered by _should_sync_amax_across_ep), so Megatron's numerics are unchanged. Three changes:

  • A rank joins the data-parallel sync even without an amax.
  • The tensor-parallel sync, of linears and of the KV-cache quantizers, likewise runs on every rank whether or not it calibrated the quantizer. It is gated on the quantizer being enabled, which the config makes the same on every rank, instead of on a local amax; on main a calibrated rank waited for a peer that skipped it.
  • HF fused experts (_share_input_amax_across_ep) share their input amax over the EP group before the completeness check, so experts that received no tokens take the layer's value instead of failing it. Megatron's experts keep main's check-then-sync, including its "MoE calibration incomplete" error.

A failed amax all-reduce now names its likely cause: NCCL running out of memory for its buffers.

Usage

N/A: internal to calibration.

Testing

  • tests/unit/torch/quantization/test_dist.py, under gloo on CPU, two ranks, so every PR runs the multi-rank cases:

    • a rank that never calibrated still joins the sync;
    • a calibrated zero amax survives;
    • every rank ends in the calibrator's dtype, and calibrated ranks take the group max, for an INT8 and an NVFP4 dynamic-block quantizer (the latter synced as per-tensor);
    • a quantizer no rank calibrated stays uncalibrated, per-tensor and per-channel;
    • uneven per-channel calibration fails on every rank, on a group with a 10 s timeout, so a rank left waiting fails rather than hangs; the jobs destroy the groups they create, so the session's pooled workers do not keep them;
    • a rank without an amax still joins the tensor-parallel sync, for a column-parallel linear and for the KV-cache quantizers;
    • only modules that opt in share expert input amax before the completeness check, while Megatron's raise as on main.

    Each fails on the code before its fix, or under a mutation of the sync it checks.

  • tests/gpu/torch/quantization/test_amax_sync_distributed.py keeps only what needs NCCL: a rank without an amax contributes its zero on the collective's CUDA device, when one rank calibrated and when none did (2 GPUs; the other multi-rank cases moved to the unit test above, since the PR runner has 1 GPU).

  • The GPU results here were measured on earlier revisions of this branch, including the earlier, larger GPU file (7 passed on a 4-GPU node); this revision's GPU tests run in CI only.

  • Against main, inside a one-rank process group, as Megatron, FSDP2 and DDP runs calibrate: FP8 max, FP8 MSE and INT8 MSE calibration leave every amax value and dtype identical to main's (48 of 48). An FP8 export with an all-zero weight succeeds, as on main.

  • A calibrated zero amax and the uncalibrated quantizers are now one parametrized test, and the never-calibrated-rank test, a case the dtype test runs, is dropped. Each drop is checked by mutation.

  • Locally on this revision's content (the series on main 6cd9ced, restacked unchanged onto main 0638a27 after [1/21] merged): pre-commit (every hook, mypy included) over each PR of the stack against main, and test_dist.py with the transport's tests on transformers 5.18 and 5.5. CI runs the full unit lanes and the docs build.

  • The series' model runs (see [21/21]) calibrate sparsely routed MoEs at 4 to 64 ranks through this path.

Before your PR is "Ready for review"

  • Is this change backward compatible?: ✅ Single-process calibration and Megatron's synchronization are unchanged.
  • If you copied code from any other sources or added a new PIP dependency, did you follow guidance in CONTRIBUTING.md: N/A
  • Did you write any new necessary tests?: ✅
  • Did you update Changelog?: N/A. Covered by the feature's entry in [21/21].
  • Did you get Claude approval on this PR?: ❌ Not yet.

Additional Information

Part 2 of 21 of expert-parallel PTQ for Hugging Face MoE models. [1/21] is merged, so this PR is now the bottom of the stack, on main.

Summary by CodeRabbit

  • Bug Fixes
    • Improved distributed quantization calibration so ranks can synchronize values even when some ranks have no local calibration data.
    • Calibration remains unset when no ranks have data, preserves calibrated zero values and data types, and flags inconsistent per-channel calibration across ranks.
    • Expert-parallel calibration shares input values before MoE completeness checks where configured.
    • Tensor-parallel and KV-cache synchronization now handles partially calibrated ranks correctly.
    • Distributed synchronization failures now raise a contextual error instead of being reduced to a warning.

@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Distributed amax synchronization now includes ranks with missing local calibration values. Opted-in expert modules synchronize enabled input quantizers across expert-parallel groups before MoE completeness validation. Tests cover dtype, zero, missing, uneven, tensor-parallel, KV-cache, and expert-input cases.

Changes: Distributed Amax Calibration

Layer / File(s) Summary
Coordinate amax across ranks
modelopt/torch/quantization/nn/modules/tensor_quantizer.py, tests/gpu/torch/quantization/test_amax_sync_distributed.py, tests/unit/torch/quantization/test_dist.py
TensorQuantizer.sync_amax_across_distributed_group coordinates calibration presence, amax values, and dtype metadata across ranks. Static non-per-tensor quantizers raise on uneven calibration. Per-tensor and dynamic-block quantizers propagate calibrated values to uncalibrated ranks.
Synchronize opted-in expert inputs
modelopt/torch/quantization/model_calib.py, modelopt/torch/quantization/plugins/huggingface.py, tests/unit/torch/quantization/test_dist.py
max_calibrate synchronizes enabled input quantizers for opted-in QuantModules before MoE completeness validation. _QuantFusedExperts opts in. Tensor-parallel and KV-cache synchronization also includes enabled quantizers without local amax values.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~45 minutes

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant max_calibrate
  participant QuantModule
  participant TensorQuantizer
  participant EPGroup
  max_calibrate->>QuantModule: Check _share_input_amax_across_ep
  QuantModule-->>max_calibrate: Return sharing flag
  max_calibrate->>TensorQuantizer: Synchronize enabled input quantizers
  TensorQuantizer->>EPGroup: Reduce calibration state and amax
  EPGroup-->>TensorQuantizer: Return reduced state and amax
  max_calibrate->>max_calibrate: Check MoE calibration completeness
Loading

Suggested reviewers: kevalmorabia97

Merge Risk: 🟡 Moderate · up to 13838

Distributed calibration now keeps ranks without local statistics in the synchronization step. However, for per-channel quantizers configured through block sizes, tensor-parallel ranks can disagree about whether to join the synchronization, which can hang the run. Make tensor-parallel participation depend on configuration rather than on state that changes during forward before merging. The earlier test-import concern is fixed.

Pre-merge checks: Passed 5 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 51.85% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 27 functions across 5 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 5 passed
Check name Status Explanation
Title check Passed The title clearly and concisely describes the main change: correcting amax synchronization for data-parallel calibration.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed PASS. The reviewed range changes three ModelOpt Python modules and no examples or dependency files. Added code only implements distributed amax synchronization and an expert-sharing flag. The added di…
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Full details: Security Anti-Patterns

Explanation

PASS. The reviewed range changes three ModelOpt Python modules and no examples or dependency files. Added code only implements distributed amax synchronization and an expert-sharing flag. The added diff contains no torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, external-input eval()/exec(), or # nosec. SECURITY.md states these rules apply to non-test code, and the dependency-file diff is empty.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

@shengliangxu
shengliangxu added this pull request to stack #2755 October 12, 2026 00:57
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from 06a5f2d to 5f115f2 Compare October 12, 2026 02:35
@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2744/

Built to branch gh-pages at 2026-10-24 01:33 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 5f115f2

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.49123% with 2 lines in your changes missing coverage. Please review.
✅ Project coverage is 79.13%. Comparing base (0638a27) to head (6760929).

Files with missing lines Patch % Lines
.../torch/quantization/nn/modules/tensor_quantizer.py 95.45% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2744      +/-   ##
==========================================
- Coverage   79.82%   79.13%   -0.69%     
==========================================
  Files         661      661              
  Lines       74928    74977      +49     
==========================================
- Hits        59810    59332     -478     
- Misses      15118    15645     +527     
Flag Coverage Δ
examples-diffusers 19.90% <17.54%> (-0.05%) ⬇️
examples-gpt-oss 13.44% <7.01%> (-0.03%) ⬇️
examples-hf_ptq 23.33% <17.54%> (-0.09%) ⬇️
examples-llm_distill 13.52% <7.01%> (-0.04%) ⬇️
examples-llm_eval 17.16% <17.54%> (-0.04%) ⬇️
examples-llm_qat 17.34% <17.54%> (-0.07%) ⬇️
examples-llm_sparsity 16.75% <7.01%> (-0.04%) ⬇️
examples-megatron_bridge 26.41% <52.63%> (-0.24%) ⬇️
examples-specdec_bench 13.22% <7.01%> (-0.03%) ⬇️
examples-speculative_decoding 17.42% <17.54%> (-0.10%) ⬇️
examples-torch_onnx 21.21% <15.78%> (-0.07%) ⬇️
examples-torch_trt 15.14% <17.54%> (-0.04%) ⬇️
examples-vllm_serve 17.38% <17.54%> (-0.04%) ⬇️
gpu 58.92% <75.43%> (-1.16%) ⬇️
regression 15.04% <7.01%> (-0.04%) ⬇️
unit 61.16% <94.73%> (+0.04%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from 5f115f2 to d6a4b30 Compare October 12, 2026 08:39
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d6a4b30

@shengliangxu shengliangxu changed the title [9/19] Make amax synchronization correct under data-parallel calibration [OMNIML-6072] [9/19] Make amax synchronization correct under data-parallel calibration Oct 12, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from d6a4b30 to e28bd09 Compare October 13, 2026 00:53
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from e28bd09 to 235ecbd Compare October 13, 2026 01:32
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 235ecbd

@shengliangxu
shengliangxu removed this pull request from stack #2755 October 13, 2026 17:42
@shengliangxu
shengliangxu added this pull request to stack #2775 October 13, 2026 17:43
@shengliangxu shengliangxu changed the title [OMNIML-6072] [9/19] Make amax synchronization correct under data-parallel calibration [OMNIML-6072] [9/20] Make amax synchronization correct under data-parallel calibration Oct 13, 2026
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from 235ecbd to 9572dc5 Compare October 13, 2026 19:38
@shengliangxu
shengliangxu removed this pull request from stack #2775 October 13, 2026 19:38
@shengliangxu
shengliangxu added this pull request to stack #2781 October 13, 2026 19:40
@shengliangxu shengliangxu changed the title [OMNIML-6072] [9/20] Make amax synchronization correct under data-parallel calibration [OMNIML-6072] [10/21] Make amax synchronization correct under data-parallel calibration Oct 13, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 9572dc5

@shengliangxu
shengliangxu removed this pull request from stack #2781 October 14, 2026 00:13
@shengliangxu
shengliangxu force-pushed the shengliangx/hfep-7-amax-sync branch from 9572dc5 to 6e7ca4f Compare October 14, 2026 00:14
@shengliangxu
shengliangxu added this pull request to stack #2786 October 14, 2026 00:14
@shengliangxu shengliangxu changed the title [OMNIML-6072] [10/21] Make amax synchronization correct under data-parallel calibration [OMNIML-6072] [11/22] Make amax synchronization correct under data-parallel calibration Oct 14, 2026
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 4f9e94b

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 669b19c

@Fridah-nv

Copy link
Copy Markdown
Contributor

Design suggestion (non-blocking): one sync entry point for calibration

#2736 gives the control plane labeled, fail-together collectives. Calibration doesn't get that yet: max_calibrate walks the modules and issues the DP, EP and TP syncs itself, and TensorQuantizer.sync_amax_across_distributed_group uses raw dist.all_reduce. The hangs in the deck (amax dtype mismatch, TP sync gated on the amax) came from this layer, and a hang there doesn't show whether the algorithm or the distribution is at fault.

Suggestion (EP only for now; TP/DP can move behind it later):

  1. Add one entry point, e.g. sync_calibration_stats(model), that reads the recorded plan and _parallel_state and does whatever sync the layout needs. Algorithms call it and never touch a process group. It is a no-op without a plan.
  2. Label its collectives per stage (e.g. per decoder layer) and carry a failure flag in the presence-flag all-reduce you already pack. A failing rank then raises everywhere, naming the stage.
  3. Raise when an algorithm that isn't synced under EP (GPTQ, MSE, local-Hessian?) runs with an EP plan, instead of running unsynced.

Which algorithms are meant to work under EP in this stack?

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 838be3c

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 719a36d

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

@Fridah-nv, thanks. Which algorithms work under expert parallelism, as of this push:

  • Supported: max syncs its amax over the data-, expert- and tensor-parallel groups (this PR). GPTQ merges its Hessians across each module's data-parallel replicas. MSE and SmoothQuant work from max's synced amax.
  • Refused, new in [OMNIML-6071] [8/21] Calibrate a distributed model on per-rank data #2740:
    • AWQ (awq_lite, awq_clip, awq_full, and SVDQuant, which runs awq_lite) averages its activation scales over data parallelism, but picks each layer's scale from its own rank's search. A 2-rank probe confirmed that the replicas diverge.
    • local_hessian and imatrix share nothing between ranks, and neither do max and MSE with distributed_sync turned off.
    • mtq.quantize and mtq.calibrate now refuse all of these on a model whose recorded plan spans several ranks, before converting anything, as your point 3 suggests.
  • The full matrix, including what has run on GPUs, is in [OMNIML-6085] [21/21] Run expert-parallel PTQ from hf_ptq.py #2754's description.

On points 1 and 2, one sync entry point that reads the plan, with labeled, fail-together collectives: agreed on the direction, as a follow-up. It would reshape max_calibrate's DP/EP/TP syncs, which Megatron shares, so we'd rather not grow this series with it.

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 481b71b

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test d11f0f8

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 7408fe2

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test ef0ff99

With data-parallel calibration a module can be exercised on some ranks and
not others -- a sparsely routed MoE expert, or anything a given rank's batch
never reaches -- so _amax ends up present on only a subset of the group. Every
rank must still join the collective or the participant set mismatches and NCCL
hangs, which makes "what does a rank with no amax contribute" the whole
problem.

For a per-tensor amax the answer is easy: the shape is known locally, so a
have-not contributes a scalar zero, the identity for MAX over non-negative
amax. If the all-reduced result is zero then nobody calibrated it, and the
buffer is dropped so downstream `amax is None` handling still applies --
keeping it as 0 would quantize the module to nothing.

A static per-channel or per-block amax has no locally-known shape, so a
have-not cannot fabricate a correctly-shaped zero. Ranks agree first, via one
MAX all-reduce of a packed presence flag that every rank joins: nobody
calibrated it is fine and skipped; everybody did falls through to the real
reduce; a genuine split raises on every rank at once. Raising only where the
amax is missing would leave the other ranks blocked in a collective that never
completes.

A dynamic-block quantizer -- NVFP4 activations -- stores only a scalar global
scale; its block_sizes drive runtime scales and do not describe the stored
amax. Classifying by "has block_sizes" would push it down the static path and
turn the ordinary uneven-calibration case into a hard error, so it is treated
as per-tensor.

Every rank also contributes the same dtype and device. A calibrated amax keeps
the calibrator's dtype while a have-not's zero is float32, and NCCL hangs on a
mismatch rather than raising (seen on Kimi-K3, NVFP4 EP64: 8 ranks with an
amax, 56 without, and the collective never returned), so the amax is cast to
float32 first. The device comes from the group's backend, which every rank
shares -- CUDA under NCCL, CPU otherwise -- so a gloo group on a CPU host works.

max_calibrate keeps main's sync structure -- DP always, EP filtered by
_should_sync_amax_across_ep -- so Megatron's numerics are unchanged, with
three changes: a rank joins the DP sync even without an amax (disabled
quantizers are skipped, which is symmetric since it is a config property);
the input amax of expert-parallel experts is shared over the EP group BEFORE
the completeness check, so experts that received no tokens pick up the
layer's value instead of failing it; and the allocator's cached pool is
returned first, since NCCL allocates its buffers outside it. A failed amax
all-reduce now says it is most likely that out-of-memory rather than
surfacing as a bare CUDA error.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Two reads go through an Optional that is set by then, which the mypy run in
pre-commit (and so in CI) rejects: the amax every rank holds once it reaches
the MAX all-reduce (calibrated, or the zero synthesised just above), and the
parallel state _has_expert_parallelism has already checked. Assert both.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The sync decided that no rank had calibrated a quantizer when the reduced
amax was zero, and dropped it. A calibrated amax can legitimately be zero --
an all-zero weight -- and dropping it made the FP8 export fail on the missing
_amax for any run with a process group (Megatron, FSDP2, DDP). The ranks now
agree through a presence flag carried in the same all-reduce.

The sync also promoted every amax to float32 for the collective and left it
there, so later bf16 writes (MSE calibration) stopped rounding as before.
Each rank now keeps its calibrator's dtype, and a rank that never calibrated
the quantizer adopts the group's.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
…he release

Sharing the routed experts' input amax over the expert-parallel group before
the completeness check exists for HF fused experts, whose experts see only
the tokens routed to them. It also applied to Megatron's experts, which then
stopped failing on incomplete calibration and silently took the group max.
Only modules that opt in (_share_input_amax_across_ep, set by the HF fused
experts wrapper) share early; Megatron keeps main's check-then-sync.

max_calibrate also ran gc.collect() and emptied the CUDA cache on every call,
which AWQ and SmoothQuant make once per linear: ~45 s on an 8B model. The
distributed forward loop already returns the cache after its last forward,
right before the sync this was for.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
With one rank nobody lacks the amax, so nothing raises and pytest.raises fails inside the worker. That failure is a BaseException the worker pool does not catch: the worker dies without reporting, and every later test on the session's pool waits on it. CI's PR GPU runner has one GPU.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Each of them spawned two ranks, about 14 s of the linux unit job apiece.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The amax sync is a collective every rank of the group joins, and it
handles a rank with no amax of its own. The tensor-parallel callers still
skipped such a rank, for linears and for the KV-cache quantizers alike, so
a rank that calibrated the quantizer waited in the sync for a peer that
never joined it. They now gate on whether the quantizer is enabled, as the
data-parallel and expert-parallel sync does: the config enables the same
quantizers on every rank, and a disabled one, which nothing quantizes
with, joins no collective.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The GPU test file sized its world from the GPU count, so on the 1-GPU PR
runner each "some ranks missing" case ran on one rank that held the
amax, and its one two-rank case needed two GPUs: no PR exercised the
multi-rank behavior. The sync logic runs on gloo through
cpu_dist_workers instead:
- the uneven per-channel raise, which every rank must reach together,
  on a group that gives up in seconds rather than hang;
- a per-channel quantizer no rank calibrated, which is not an error;
- an NVFP4 dynamic-block quantizer, which syncs as a per-tensor one;
- the group's max over ranks that all calibrated, which no unit test
  checked: a rank keeping its own amax went unnoticed.

The GPU file keeps the check only NCCL can make, where a rank without
an amax puts the zero it contributes, and asks for two GPUs: with one,
no rank lacks the amax.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
- test_amax_sync_with_a_rank_that_never_calibrated is the one-calibrated
  half of test_the_sync_leaves_every_rank_the_calibrators_dtype, which
  runs the same case in bf16 and checks the dtype too.
- test_a_calibrated_zero_amax_survives_the_sync and
  test_a_quantizer_no_rank_calibrated_stays_uncalibrated become one
  test_the_sync_keeps_a_calibrated_zero_apart_from_uncalibrated over
  calibrated-zero, uncalibrated and uncalibrated-per-channel, each case
  keeping its assert.

A have-not rank that keeps no amax or takes zero, a zero amax read as
uncalibrated, nobody's amax becoming zero, and a per-channel quantizer
nobody calibrated raising each fail a remaining test.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
The two jobs that sync on a group with a 10-second timeout left it, with its gloo backend, registered
on the session's persistent cpu_dist_workers until the pool shut down; they now destroy it.

Signed-off-by: Shengliang Xu <shengliangx@nvidia.com>
@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/ok to test 6760929

@shengliangxu

Copy link
Copy Markdown
Collaborator Author

/claude review

Comment thread modelopt/torch/quantization/nn/modules/tensor_quantizer.py
Comment thread modelopt/torch/quantization/nn/modules/tensor_quantizer.py
Comment thread modelopt/torch/quantization/model_calib.py
# rank skips and another syncs pairs mismatched tensors in every later
# all_reduce, which NCCL hangs on rather than reporting.
if leaf.is_enabled:
leaf.sync_amax_across_distributed_group(ep_group)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION] The block comment above states the contract as "sync_amax_across_distributed_group fills a None rank with the group MAX; if NO rank saw tokens the amax is dropped group-wide". That only holds for a per-tensor (or dynamic-block) quantizer. For an input quantizer with axis is not None or a static block_sizes, the new implementation takes the static branch and the cold-expert rank hits the "unevenly-calibrated non-per-tensor quantizer" RuntimeError instead — i.e. exactly the case this loop was added to make work turns into a hard failure, with an error message pointing the user at --calib-size even though no calibration size can fix a cold EP shard.

Shipped MoE activation recipes (FP8 per-tensor, NVFP4 dynamic-block) are all per-tensor-amax, so nothing today is affected; but a per-channel activation recipe on fused experts would regress to an error. Either note the restriction in the comment, or make it explicit where it is cheap to check — e.g. skip-with-warning rather than raise when the quantizer is a routed-expert input quantizer being shared over EP.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Postponed to a follow-up: the comment should limit the fill to a per-tensor (or dynamic-block) amax. A static per-channel or per-block input amax that only some ranks hold raises on every rank together instead of hanging, and, as you note, no shipped recipe calibrates an expert input that way, so this approved slice keeps the comment as it is.

@claude

claude Bot commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

Claude review summary

Scope reviewed: full PR (5 files, +358/-15) — modelopt/torch/quantization/model_calib.py, nn/modules/tensor_quantizer.py, plugins/huggingface.py, plus both test files. Everything was opened; no paths were skipped. Traced sync_amax_across_distributed_group to all of its call sites (max_calibrate DP/EP/TP/KV syncs and apply_pre_quant_scale_and_smooth), checked is_enabled against enable_stats_collection/finish_stats_collection to confirm what the new gate means at sync time, and checked _QuantFusedExperts to confirm the opt-in flag reaches real input_quantizer children.

Findings: CRITICAL 0, IMPORTANT 0, SUGGESTION 4.

Assessment

The core design holds up. The three things I checked hardest all came out right:

  • Symmetry of the new gate. Replacing _amax is None with is_enabled is sound: is_enabled at sync time is not _disabled, which is set from config and from enable_stats_collection (no-calibrator quantizers get disabled there) — both identical on every rank. The old gate was the actual hazard: a rank with an amax would enter all_reduce while a peer without one returned, which is a hang under NCCL. This PR converts those hangs into either a correct reduce or a symmetric raise.
  • The [present, -present] packing. MAX over -present yields -min(present), so any_present = flag[0] > 0 and all_present = -flag[1] > 0 are both correct, and the flag is read from presence rather than from the amax value — which is what keeps a legitimately calibrated zero (all-zero weight) distinct from never-calibrated. The parametrized test covers exactly that distinction.
  • Device and dtype derivation. _collective_device deriving from the group backend rather than from a local tensor is the right call, since the have-not rank has no tensor to ask; the "nccl" in str(get_backend(group)) form also handles the mixed cpu:gloo,cuda:nccl default group. The float32-throughout choice for the per-tensor path is well justified by the Kimi-K3 EP64 hang.

Test coverage is proportionate and the multi-rank cases run on gloo so every PR exercises them; using a 10 s group timeout so a left-behind rank fails instead of hanging CI is the right pattern per the contributing guide. The GPU file keeps only what needs NCCL.

The suggestions

None block. In rough order of usefulness:

  1. The static path assumes matching shape and dtype across ranks but a mismatch hangs rather than raises — the presence-flag all-reduce it already pays for could carry numel and a dtype code and turn that into a symmetric error for free.
  2. A float64 amax loses precision through the float32 packing (the only lossy entry in _AMAX_DTYPES), and a cold rank gets its new _amax buffer on the collective device rather than on the module device.
  3. The EP-share loop uses (TensorQuantizer, SequentialQuantizer) where its two neighbouring loops use the already-imported AnyQuantizer, dropping GroupedQuantizer.
  4. The EP-share comment phrase "fills a None rank with the group MAX" holds only for per-tensor amax; a per-channel expert input quantizer would hit the unevenly-calibrated raise instead.

One out-of-diff note

_amax_for_smoothing (the per-channel activation amax that SmoothQuant and AWQ fold into pre_quant_scale) is not synced across the data-parallel group at all. Under data-parallel calibration a module some ranks never exercised therefore gets a rank-dependent pre_quant_scale folded into its weights, which diverges the replicas. That is pre-existing on main and outside this diff, but it is the same class of bug the PR title describes, so it may be worth a follow-up in the series.

Risk: low. Behaviour at world size 1 and for Megatron is unchanged, the backward-compatible direction is preserved, and the new failure modes are loud rather than silent.

🤖 Generated with Claude Code

@claude claude Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude review passed — no blocking issues found. LGTM

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants